<?php
// +-------------------------------------------------------------------
// | 
// +-------------------------------------------------------------------
// | Copyright (c) 2009-2017 http://www.kcdns.com All rights reserved.
// +-------------------------------------------------------------------

// 日志
// KCSLog::init();
// KCSLog::DEBUG('info');
// KCSLog::ERR($errno, $errstr, $errfile, $errline);
// KCSLog::TRACE('trace');
// KCSLog::APP('payment-notice',true,array('data'=>1));
// 捕获错误日志需要手动激活 : set_error_handler(function () { call_user_func_array(array( 'KCSLog', 'ERR' ), func_get_args()); });
class KCSLog
{

    protected static $config = array(
            // 日志路径
            'LOG_PATH' => './',

            // 日志级别 : ERR,WARN,APP,DEBUG,INFO
            'LOG_LEVEL' => 'ERR,WARN,APP,DEBUG,INFO',

            // 应用统计日志级别, 以下级别的日志单独记录为 json 格式
            'LOG_CENTER_LEVEL' => 'ERR,APP,WARN',

            // 应用统计日志路径
            'LOG_CENTER' => '/tmp/app_log',

            // PHP 错误日志级别, 以下级别的记录为 ERR 日志 : ERROR,WARNING,NOTICE
            'LOG_ERR_LEVEL' => 'ERROR,WARNING',

            // 记录日志调用位置
            'LOG_LINE' => true,

            // 日志调用位置相对路径
            'LOG_LINE_PREFIX' => '',

            // 客户端 ip
            // true:使用前端代理的环境自动获取
            // false:使用最后一次握手IP
            // 其他 : 指定客户端 ip
            'CLIENT_IP' => true,

            // 全局数组变量, 存储性能日志
            // $GLOBALS['_KCSLOG_PROFILE_'] = ['DB_READ'=>10,'DB_WRITE'=>'10'];
            'PROFILE' => '_KCSLOG_PROFILE_',

            // 脚本执行超过此时间时记录 WARNING 日志, 单位 ms
            'PROFILE_TIME_LIMIT' => 300,

            // 记录请求参数 : GET / POST / COOKIE / SESSION
            'LOG_REQUEST' => true,

            // 日志文件后缀
            'LOG_SUFFIX' => '.log',

            // 日志文件大小, 自动分割, 默认 1M
            'LOG_SIZE' => 1048576
    );

    // 日志项
    protected static $items = array();

    // 当前时间戳
    protected static $time;

    // 当前时间字符串
    protected static $now;

    // 客户端 IP
    protected static $ip = '0.0.0.0';

    // 日志 ID
    public static $id = '';

    // 日志初始化
    public static function init ($config = array())
    {
        @date_default_timezone_set("PRC");
        self::$config = array_merge(self::$config, $config);
        self::$time = date('Y-m-d H:i:s');
        self::$now = microtime(true);

        self::$config['LOG_LEVEL'] = strtoupper(self::$config['LOG_LEVEL']);
        self::$config['LOG_CENTER_LEVEL'] = strtoupper(self::$config['LOG_CENTER_LEVEL']);
        self::$config['LOG_ERR_LEVEL'] = strtoupper(self::$config['LOG_ERR_LEVEL']);

        // 日志目录
        is_dir(self::$config['LOG_PATH']) or @mkdir(self::$config['LOG_PATH'], 0777, true);
        self::$config['LOG_PATH'] = realpath(self::$config['LOG_PATH']) . '/';

        // 日志中心目录
        is_dir(self::$config['LOG_CENTER']) or @mkdir(self::$config['LOG_CENTER'], 0777, true);
        self::$config['LOG_CENTER'] = realpath(self::$config['LOG_CENTER']) . '/';

        // 默认根据入口文件计算相对路径
        if (self::$config['LOG_LINE'])
        {
            if (! self::$config['LOG_LINE_PREFIX'])
            {
                $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
                $endTrace = end($trace);
                self::$config['LOG_LINE_PREFIX'] = dirname($endTrace['file']);
            }
            else
            {
                self::$config['LOG_LINE_PREFIX'] = realpath(self::$config['LOG_LINE_PREFIX']);
            }
        }

        self::$id = date('His-') . uniqid();

        register_shutdown_function(array(
                __CLASS__,
                '_save'
        ));
    }

    // 记录 PHP 错误日志
    protected static function ERR ($errno = null, $errstr = null, $errfile = null, $errline = null)
    {
        switch ($errno)
        {
            case E_ERROR:
            case E_PARSE:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_USER_ERROR:
                $type = "ERROR";
                $appLevel = 'ERR';
                break;
            case E_WARNING:
            case E_CORE_WARNING:
            case E_COMPILE_WARNING:
            case E_USER_WARNING:
                $type = "WARNING";
                $appLevel = 'WARN';
                break;
            default:
                $type = 'NOTICE';
                $appLevel = 'DEBUG';
        }

        // 脚本异常结束时只记录 error 错误
        if (strpos(self::$config['LOG_ERR_LEVEL'], $type) !== false)
        {
            self::_record("{$type} : {$errstr}", "ERR", $errfile . ':' . $errline, $appLevel);
        }
    }

    // 记录日志中心日志
    protected static function APP ($info = '', $status = true, $extend = null)
    {
        if ($info === '')
        {
            return false;
        }

        $message = [
                'info' => $info,
                'status' => $status,
                'extend' => $extend
        ];

        self::_record($message, 'APP');
    }

    // 记录调用栈
    protected static function TRACE ($message = '')
    {
        $trace = debug_backtrace();
        $info = array();
        $tMessage = array();
        $line = '';
        for ($i = count($trace) - 1; $i > - 1; $i --)
        {
            $v = $trace[$i];
            if ($v['class'] === __CLASS__)
            {
                $line = $v['file'] . ':' . $v['line'];
                break;
            }

            $v['args'] = isset($v['args']) ? $v['args'] : array();
            $row = "{$v['file']}({$v['line']}): ";
            isset($v['class']) and $row .= "{$v['class']}{$v['type']}";
            $row .= "{$v['function']}(" . implode(',', $v['args']) . ")";
            $info[] = $row;
        }
        $infoCount = count($info);
        for ($i = $infoCount - 1; $i > - 1; $i --)
        {
            $tMessage[] = "\t\t\t#" . str_pad(($infoCount - $i - 1), 2, ' ') . " {$info[$i]}";
        }
        $tMessage[] = "\t\t\t#{$infoCount} {main}";
        self::_record($message . "\n" . implode("\n", $tMessage), 'TRACE', $line);
    }

    // 记录本地日志
    public static function __callStatic ($method, $param)
    {
        $method = strtoupper($method);
        if (strpos(self::$config['LOG_LEVEL'], $method) === false)
        {
            return false;
        }

        $message = isset($param[0]) ? $param[0] : '';
        $line = isset($param[1]) ? $param[1] : '';

        // 传入异常时重新构造消息
        if ($message instanceof \Exception)
        {
            $e = $message;
            $message = $e->getMessage() . "\n" . $e->getTraceAsString();
            $line = $e->getFile() . ':' . $e->getLine();
        }

        method_exists(__CLASS__, $method) ? call_user_func_array(array(
                __CLASS__,
                $method
        ), $param) : self::_record($message, $method, $line);
    }

    protected static function _record ($message = '', $level = 'DEBUG', $line = '', $appLevel = '')
    {
        // 记录调用位置
        $items = array(
                $level,
                $message,
                microtime(true),
                null,
                $appLevel
        );
        if ($line === '' && (self::$config['LOG_LINE'] || is_null($message)))
        {
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5);
            for ($i = count($trace) - 1; $i > - 1; $i --)
            {
                if ($trace[$i]['class'] === __CLASS__)
                {
                    isset($trace[$i]['file']) and $line = $trace[$i]['file'] . ':' . $trace[$i]['line'];
                    break;
                }
            }
        }

        $line && $items[3] = $line;
        foreach ($items as &$v)
        {
            // 清理日志中的路径字符串时, 不区分大小写, 解决 windows 下盘符可能出现不同大小写的问题
            $v = str_ireplace(self::$config['LOG_LINE_PREFIX'], "", $v);
        }
        self::$items[] = $items;
    }

    public static function _save ($type = '', $destination = '')
    {
        error_reporting(0);
        if ($e = error_get_last())
        {
            self::ERR($e['type'], $e['message'], $e['file'], $e['line']);
        }

        if (PHP_SAPI != 'cli' and (($cost_time=(microtime(true) - self::$now)) * 1000 > self::$config['PROFILE_TIME_LIMIT']))
        {
            self::WARN("RUNTIME OVER LIMIT " . self::$config['PROFILE_TIME_LIMIT'] . "ms cost_time={$cost_time}s");
        }

        if (empty(self::$items))
        {
            return;
        }

        // 记录性能信息
        if (self::$config['PROFILE'])
        {
            $_profiles = array();
            if (isset($GLOBALS[self::$config['PROFILE']]))
            {
                foreach ($GLOBALS[self::$config['PROFILE']] as $k => $v)
                {
                    $_profiles[] = "{$k}:{$v}";
                }
            }
            self::$items[] = array(
                    'PROFILE',
                    implode(' / ', $_profiles),
                    microtime(true)
            );
        }

        self::$ip = self::_getIp();
        self::_saveSystemLog();
        self::_saveCenterLog();
        self::$items = array();
    }

    /**
     * 记录文本日志
     */
    protected static function _saveSystemLog ()
    {
        // 日志文件
        $destination = self::$config['LOG_PATH'] . date('y_m_d') . self::$config['LOG_SUFFIX'];

        // 每个日志文件最大 1M
        if (is_file($destination) && (self::$config['LOG_SIZE'] <= filesize($destination)))
        {
            $newSperator = strpos(PHP_OS, 'WIN') !== false ? '-' : ':';
            $newDestination = dirname($destination) . '/' . substr(basename($destination), 0, - 4) . '_[' . date("H{$newSperator}i{$newSperator}s") . ']' . self::$config['LOG_SUFFIX'];
            $st = copy($destination, $newDestination);
            $st and unlink($destination);
        }

        if (PHP_SAPI == 'cli')
        {
            $req = 'CLI ' . implode(' ', $GLOBALS['argv']);
        }
        else
        {
            $req = $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'];
        }

        $log = "[" . self::$time . "] " . $req . ' ' . self::$ip . ' ' . self::$id . "\r\n";

        foreach (self::$items as $v)
        {
            $tag = str_pad("[{$v[0]}]", 9, ' ');
            $info = is_string($v[1]) ? $v[1] : var_export($v[1], true);

            $latencyStr = '';
            if ($v[2])
            {
                list ($second, $microSecond) = explode('.', $v[2] - self::$now);
                $microSecond = number_format('0.' . $microSecond, 3) * 1000;
                $second >= 10 and $latencyStr = $second . "s";
                $second >= 1 and $second < 10 and $latencyStr = "{$second}." . $microSecond . "s";
                $second < 1 and $latencyStr = "{$microSecond}ms";
                $latencyStr = str_pad("[+$latencyStr] ", 9, ' ');
            }

            $line = $v[3] ? " [{$v[3]}]" : "";
            $log .= "{$tag} {$latencyStr}{$info}{$line}\r\n";
        }
        self::_write($destination, $log . "\r\n");
    }

    /**
     * 记录应用统计日志
     */
    protected static function _saveCenterLog ()
    {
        foreach (self::$items as $v)
        {
            strpos(self::$config['LOG_CENTER_LEVEL'], $v[0]) !== false and $log .= json_encode(self::_formartItem($v)) . "\r\n";
        }
        $destination = self::$config['LOG_CENTER'] . 'APP_' . date('y_m_d') . '.log';

        self::_write($destination, $log);
    }

    /**
     * 格式化应用统计日志
     * { *appname, *package, *request, *ip, *file, *line, *mothed, *info, *time, *status, *level }
     */
    protected static function _formartItem ($v = '')
    {
        $line = $v[3] ? explode(':', $v[3]) : [
                '-',
                '-'
        ];

        $info = is_array($v[1]) ? $v[1] : [
                'info' => $v[1]
        ];

        if (PHP_SAPI == 'cli')
        {
            $item['appname'] = __DIR__;
            $item['mothed'] = "CLI";
            $item['request'] = implode(' ', $GLOBALS['argv']);
        }
        else
        {
            $item['appname'] = $_SERVER['HTTP_HOST'];
            $item['mothed'] = $_SERVER['REQUEST_METHOD'];
            $item['request'] = $_SERVER['REQUEST_URI'];
        }

        $item['package'] = MODULE_NAME . '.' . CONTROLLER_NAME . '.' . ACTION_NAME;
        $item['ip'] = self::$ip;
        $item['file'] = $line[0];
        $item['line'] = $line[1];
        $item['info'] = $info['info'] ?  : '-';
        $item['time'] = date('Y-m-d H:i:s');
        // $item['status'] = isset($info['status']) && $info['status'] ? 'ok' : 'error';
        $item['status'] = '';
        switch ($v[0])
        {
            case 'ERR':
            case 'WARN':
            case 'DEBUG':
                $item['level'] = $v[4] ?  : $v[0];
                break;
            default:
                $item['level'] = 'INFO';
        }
        $item['extend'] = $info['extend'] ?  : '-';

        return $item;
    }

    /**
     * 写入日志文件
     */
    protected static function _write ($destination, $log = '')
    {
        $log && error_log($log, 3, $destination);
    }

    /**
     * 获取客户端 IP
     */
    protected static function _getIp ()
    {
        // 使用前端代理
        if (self::$config['CLIENT_IP'] === true)
        {
            // 通过前端代理服务器获取
            if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
            {
                $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
                $pos = array_search('unknown', $arr);
                if (false !== $pos)
                    unset($arr[$pos]);
                $ip = trim($arr[0]);
            }
            // 通过前端代理服务器获取
            elseif (isset($_SERVER['HTTP_CLIENT_IP']))
            {
                $ip = $_SERVER['HTTP_CLIENT_IP'];
            }
            // 最后一次握手 IP
            elseif (isset($_SERVER['REMOTE_ADDR']))
            {
                $ip = $_SERVER['REMOTE_ADDR'];
            }
        }
        // 不使用前端代理
        elseif (self::$config['CLIENT_IP'] === false)
        {
            $ip = $_SERVER['REMOTE_ADDR'];
        }
        // 传入 IP
        else
        {
            $ip = self::$config['CLIENT_IP'];
        }

        // IP地址合法验证
        return sprintf("%u", ip2long($ip)) ? $ip : '0.0.0.0';
    }
} 